Skip to content

fix: an ignored node is never pruned, moved or rewritten - #25

Merged
aralroca merged 2 commits into
mainfrom
fix/ignored-nodes-survive-the-prune
Aug 1, 2026
Merged

fix: an ignored node is never pruned, moved or rewritten#25
aralroca merged 2 commits into
mainfrom
fix/ignored-nodes-survive-the-prune

Conversation

@aralroca

@aralroca aralroca commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

shouldIgnoreNode only made the walk hop over a node. Everything else still treated it as an ordinary child:

  • it was counted among the old children, so extra was too high and the tail removal took one node too many;
  • the removal itself was removeChild(oldParent.lastChild), which never consulted the predicate;
  • and it could be matched against an incoming node and rewritten into it, which is the one thing a caller asking to ignore it cannot want.

The case that surfaced it

A page whose <head> holds runtime-injected stylesheets — a lazily loaded editor's CSS, a dev server's <style> tags — that the incoming page never lists. Measured on a real navigation (Janux docs, playground → docs): 55 applied stylesheets down to 0 mid-swap, then restored a beat later by the app. That round trip is a flash of unstyled content, because a re-attached stylesheet is a new, pending one and the browser paints without it until it resolves.

Protecting those nodes with shouldIgnoreNode did nothing, because the prune never looked.

The fix

The predicate is now honoured in the three places that touch old children: the count, the walk, and the removal. Ignored means untouched, in place — its content, its position and its identity all survive, while its siblings diff as usual.

Contract change

The two existing shouldIgnoreNode tests asserted that an ignored node ended up deleted, which is the opposite of what the option is named for and of what the README describes. Their expectations now pin the node surviving with its own content. A third row covers the case above: an ignored node the incoming page does not list is kept rather than pruned.

If anyone was relying on "ignored ⇒ removed", that was accidental, and there is no way to express "leave this alone" without it.

Size

The library's whole point is being tiny, so: 1494 → 1570 bytes gzip (+76 B), raw 3386 → 3603. The cost is almost entirely the walk-side skip, which is what prevents an ignored node from being rewritten; the count and prune changes are a few bytes.

Tests

bun test141 pass, 0 fail across chrome, firefox and safari (was 138). The 132 unrelated tests are untouched.

🤖 Generated with Claude Code

Aral Roca and others added 2 commits August 1, 2026 20:29
`shouldIgnoreNode` only made the walk hop over a node. Everything else still
treated it as ordinary: it was counted among the old children, so the tail
removal took one node too many, and it could be matched against an incoming
node and rewritten into it.

The case that surfaced it: a page whose head holds runtime-injected
stylesheets (a lazy editor's CSS, a dev server's <style>) that the incoming
page never lists. The prune removed all of them mid-navigation — 55 applied
sheets down to 0 — and re-attaching a stylesheet makes it pending again, so
the page paints unstyled for a frame.

Now the predicate is honoured in the three places that touch old children:
the count, the walk, and the removal. Ignored means untouched, in place.

Contract change: the two existing shouldIgnoreNode tests asserted that an
ignored node ended up deleted, which is the opposite of what the option is
for. Their expectations now pin the node surviving with its own content, plus
a new row for the case above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prune now has to ask the caller's predicate before removing an old child,
and the walk has to skip ignored nodes rather than rewrite them — 1494 -> 1549
bytes gzip. Trimmed as far as it goes without trading bytes for allocations:
settledWalker inherits the walker instead of rebuilding it, and the removal
scans back from lastChild with no extra bookkeeping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aralroca
aralroca merged commit 0e8600b into main Aug 1, 2026
1 check passed
@aralroca
aralroca deleted the fix/ignored-nodes-survive-the-prune branch August 1, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant